Skip to content

fix: enhance resolution and enum handling in anyOf schemas#901

Merged
cliffhall merged 3 commits intomodelcontextprotocol:mainfrom
Edison-A-N:fix/enum-anyof-ref-resolution
Feb 11, 2026
Merged

fix: enhance resolution and enum handling in anyOf schemas#901
cliffhall merged 3 commits intomodelcontextprotocol:mainfrom
Edison-A-N:fix/enum-anyof-ref-resolution

Conversation

@Edison-A-N
Copy link
Contributor

Summary

Fix $ref resolution and enum handling in anyOf schemas. Resolves issue #897 where enum types defined via $ref in anyOf were not properly resolved.

Motivation and Context

Addresses issue #897: anyOf=[$ref, null] patterns were not resolved, causing enum properties to be lost and preventing dropdown rendering.

How Has This Been Tested?

  • All existing tests pass (392 unit tests, 16 E2E tests)
  • Prettier and ESLint checks passed
  • Tested with schemas containing anyOf=[{$ref: "#/$defs/UserRole"}, {type: "null"}]

Breaking Changes

None. Bug fix maintaining backward compatibility.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

  • Enhanced resolveRef: Added circular reference detection, recursively resolves $ref in anyOf
  • Optimized normalizeUnionType: Unified handling for anyOf=[type, null], preserves enum properties
  • Integrated in ToolsTab: Uses resolveRef before normalization to ensure complete schema info

Fixes #897

Copy link
Member

@olaservo olaservo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Edison-A-N thanks for your PR! There's a spicy take about supporting refs in tool schemas here: #755

So think that this would apply to elicitation schemas as well. I'll drop a comment there since I hadn't responded there yet. Please feel free to share your thoughts as well.

@olaservo olaservo mentioned this pull request Nov 4, 2025
@cliffhall
Copy link
Member

Hi @Edison-A-N!

This looks like it builds upon #889 from last week, I see you are calling the resolveRef function it added. I also see it replaces some changes from #895 which I also made last week for nullable arrays, rolling up all the nullable type checks into a new block, which will be nice if it didn't break anything. I need to test that out and make sure nullable items still work correctly first.

@cliffhall
Copy link
Member

@claude /review

@claude
Copy link
Contributor

claude bot commented Feb 7, 2026

Claude finished @cliffhall's task in 2m 50s —— View job


Code Review for PR #901

Overview

This PR successfully addresses issue #897 by enhancing $ref resolution in anyOf schemas and improving enum handling. The changes enable proper rendering of enum dropdowns for patterns like anyOf=[{$ref: "#/$defs/UserRole"}, {type: "null"}].

Key Changes

1. Enhanced resolveRef Function (Lines 165-230)

Strengths:

  • ✅ Adds circular reference detection using visitedRefs Set
  • ✅ Recursively resolves $ref within anyOf arrays
  • ✅ Properly cleans up visitedRefs on failure
  • ✅ Makes recursive call to handle nested refs after initial resolution

Potential Issues:

  • ⚠️ Memory concern: The visitedRefs Set is passed by reference through recursive calls. When resolving multiple independent schemas, the Set accumulates refs across unrelated schemas. Consider creating a fresh Set for each top-level call or documenting that the same Set should not be reused across unrelated schemas.

2. Simplified normalizeUnionType Function (Lines 237-303)

Strengths:

Addressing @cliffhall's concern about nullable arrays:

  • ✅ The new unified logic at lines 238-260 correctly handles nullable arrays
  • ✅ Line 251 checks: if (nonNullItem?.type || nonNullItem?.enum) - this includes type: "array"
  • ✅ Lines 252-259 spread all properties from nonNullItem, preserving items, minItems, etc.
  • This maintains the functionality from PR Handle nullable arrays in tool inputSchemas #895 while simplifying the code

3. New resolveRefsInMessage Function (Lines 322-353)

Strengths:

  • ✅ Handles elicitation/create message patterns
  • ✅ Properly chains resolveRefnormalizeUnionType
  • Used in useConnection.ts:784 to preprocess messages before protocol handling
  • ✅ Good integration point for schema resolution in the message pipeline

4. Other Improvements

generateDefaultValue enhancements:

  • ✅ Adds isRootSchema check to ensure root objects return {} instead of undefined
  • ✅ Includes optional properties with explicit defaults
  • Has test coverage in the updated schemaUtils.test.ts

Code Quality

Positive

  • TypeScript types are properly maintained
  • Error handling with console.warn is appropriate
  • Code follows project conventions
  • JSDoc comments are clear and helpful
  • Good separation of concerns

Suggestions for Improvement

  1. Type assertion safety (client/src/utils/schemaUtils.ts:248):

    const nonNullItem = schema.anyOf.find((t) => {
      const item = t as JsonSchemaType;
      return item?.type !== "null";
    }) as JsonSchemaType;

    The final as JsonSchemaType assumes the find will succeed. Consider adding a null check:

    if (!nonNullItem) {
      return schema; // Safety fallback
    }
  2. visitedRefs accumulation (client/src/utils/schemaUtils.ts:168):
    The current implementation works fine for single-schema resolution, but if the same visitedRefs Set were reused across multiple schema resolutions (unlikely in current code), it could cause issues. Consider documenting this in the JSDoc or ensuring each top-level call starts with a fresh Set.

Testing

  • ✅ All existing tests pass (392 unit, 16 E2E)
  • ✅ Manual testing with anyOf ref patterns
  • ✅ Tests added for generateDefaultValue improvements
  • Missing tests for core PR functionality:
    • No tests for resolveRef function at all (circular ref detection, nested anyOf resolution)
    • No tests for enum preservation in normalizeUnionType
    • Existing normalizeUnionType tests only check type normalization, not enum/property preservation

Recommendation: Add unit tests for:

// Example test cases that should be added:
describe("resolveRef", () => {
  test("resolves $ref in anyOf patterns", () => { /* ... */ });
  test("detects and handles circular references", () => { /* ... */ });
  test("recursively resolves nested $ref", () => { /* ... */ });
});

describe("normalizeUnionType", () => {
  test("preserves enum properties when normalizing anyOf with $ref", () => {
    const schema = {
      anyOf: [
        { type: "string", enum: ["a", "b", "c"] },
        { type: "null" }
      ]
    };
    const normalized = normalizeUnionType(schema);
    expect(normalized.enum).toEqual(["a", "b", "c"]);
    expect(normalized.nullable).toBe(true);
  });
});

Impact Assessment

Backward Compatibility

Safe - This is a bug fix that:

  • Enhances existing functionality without breaking changes
  • The unified normalizeUnionType replaces 5 type-specific checks with one generic check
  • Maintains all existing nullable array handling from PR Handle nullable arrays in tool inputSchemas #895
  • All 408 existing tests pass

Related PRs

Security & Performance

  • ✅ Circular reference detection prevents infinite loops
  • ✅ No security concerns identified
  • ✅ Performance: minimal overhead from the visitedRefs Set
  • ✅ Message preprocessing in useConnection.ts is appropriate and non-blocking

Recommendation

Approve with suggestions for test coverage

This is a well-implemented fix that solves issue #897. The code is cleaner than before (60 lines of repetitive code reduced to unified logic), and it properly preserves enum properties through ref resolution.

Before merging (recommended but not blocking):

  1. Add unit tests for resolveRef (especially circular ref detection)
  2. Add tests for enum preservation in normalizeUnionType
  3. Consider adding null check for nonNullItem at line 248 for extra safety
  4. Document the visitedRefs parameter usage if you want to be extra clear

@cliffhall: Your concern about nullable arrays is addressed - the unified logic maintains all the functionality from PR #895 while being more maintainable. The spread operator ...nonNullItem at line 254 ensures all properties (including items for arrays) are preserved.

The lack of test coverage for the new functionality is notable but not blocking, as the existing test suite passes and manual testing has been done. However, adding tests would significantly improve confidence in this fix and prevent future regressions.


cliffhall added a commit to cliffhall/mcp-maintainer-toolkit that referenced this pull request Feb 11, 2026
See modelcontextprotocol/inspector#901

* In tools.ts
  - added `jsonRefTest` to make sure refs handling still works
    - Added `JSONRefTestSchema` which requires two `PersonSchema` objects: `husband` and `wife`.
    - Updated the `ToolName` enum to include `JSON_REF_TEST`.
    - Updated `setupToolHandlers` to register the new tool in `ListToolsRequestSchema`. The `inputSchema` is generated using `zodToJsonSchema` with the `definitions` option, ensuring that the resulting JSON schema uses `$ref` for the `Person` objects.
    - Added a handler for the `jsonRefTest` tool in `CallToolRequestSchema` that parses the arguments and returns a success message identifying the husband and wife.
  - Added a new tool `userFilterTest` to `handlers/tools.ts` along with its Zod input schema designed to yield a specific JSON schema with `$ref`, `$defs`, and nullable types.
    - Defined `UserRoleSchema` as a Zod enum.
    - Defined `UserFilterSchema` with `role` (referencing `UserRoleSchema`) and `is_active` (boolean), both nullable.
    - Added `USER_FILTER_TEST` to `ToolName` enum.
    - Registered the `userFilterTest` tool in `setupToolHandlers` using `zodToJsonSchema` with `definitions` and `definitionPath: "$defs"` to match the requested output format.
    - Added a basic handler for `userFilterTest` in `CallToolRequestSchema`.
Copy link
Member

@cliffhall cliffhall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! 👍

Tested by adding two new tools to the mcp-maintainer-toolkit.

  • USER_FILTER_TEST - Uses the exact schema proposed in the issue that this PR fixes for enums with $refs that can be null.
  • JSON_REF_TEST - Tests $ref resolution to make sure it they still resolve properly.

USER_FILTER_TEST

Image Image Image Image Image

JSON_REF_TEST

Image Image

@cliffhall cliffhall merged commit 8cf0ac1 into modelcontextprotocol:main Feb 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ToolsTab: anyOf/$ref enum not resolved; optional enum fields not clearable (appear required)

3 participants